package com.auth.manage.user.domain.service;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.CacheManager;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.template.QuickConfig;
import com.auth.manage.base.responce.SingleResponce;
import com.auth.manage.common.user.api.constant.UserStatus;
import com.auth.manage.common.user.api.request.UserModifyRequest;
import com.auth.manage.common.user.api.responce.UserResponce;
import com.auth.manage.lock.DistributeLock;
import com.auth.manage.user.constant.UserOperateTypeEnum;
import com.auth.manage.user.domain.entity.User;
import com.auth.manage.user.domain.entity.UserOperateStream;
import com.auth.manage.user.domain.entity.convertor.UserConvertor;
import com.auth.manage.user.infra.mapper.UserMapper;
import com.auth.manage.user.infra.exception.UserErrorCode;
import com.auth.manage.user.infra.exception.UserException;
import com.auth.manage.user.infra.util.BloomUtilsBean;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;

@RequiredArgsConstructor
@Service
public class UserService extends ServiceImpl<UserMapper, User> implements InitializingBean {
    private final static String NAME_PREFIX = " AuthenticationManage";

    private final BloomUtilsBean bloomUtilsBean;
    private final UserOperateStreamService userOperateStreamService;
    private final UserCacheDelayDeleteService userCacheDelayDeleteService;

    private Cache<String, User> userCache;  // 非注入
    private final CacheManager cacheManager;  // 注入，常见缓存的工厂

    public void login(String telephone, String captcha) {
        // 1.判断用户是否存在，不存在则实现注册逻辑
        SingleResponce<Object> singleResponce = new SingleResponce<>();
    }

    // todo 锁telephone，不同的telephone进来，邀请码重复接口还是会失败
    // 为了保证接口成功率，要防止重复的手机号和邀请码加唯一索引就够了
    @DistributeLock(scene = "USER_REGISTER",keyExpression = "#telephone")
    public SingleResponce<UserResponce> register(String telephone, String inviteCode) {
        // 不加锁：可能手机号重复插入，也可能不同的手机号生成了相同的邀请码，导致邀请码重复
        // 1.如果手机号不为空，判断用户是否存在
        // 登录流程在调用此接口前，已经判断过用户不存在。因此，对于登录场景，这里的校验是冗余的
        // 但是本接口也作为独立的【用户注册】入口，因此对于注册接口，做校验是必要的
        Assert.isFalse(telephoneExist(telephone), () -> new UserException(UserErrorCode.TELEPHONE_EXIST));
        // 2.生成用户名和邀请码
        String randomGenerateInviteCode;
        String userName = null;
        while (true) {
            randomGenerateInviteCode = RandomUtil.randomString(6).toUpperCase();
            if (!inviteCodeExist(randomGenerateInviteCode)) {
                userName = randomGenerateInviteCode + telephone.substring(7, 11);
                break;
            }
        }
        // 3.如果邀请码不为null，则绑定该邀请人的主键id，并且给该邀请人加积分
        Long inviterId = 0L;
        if (StringUtils.isNotEmpty(inviteCode)) {
            // 3.1 查询邀请码对应的邀请人
            User inviter = getUserByInviteCode(inviteCode);
            // 3.2 如果存在，绑定邀请人的主键id
            if (inviter != null) inviterId = inviter.getId();
            // todo 积分排行榜
        }
        // 4.插入数据库
        User user = new User();
        user.register(telephone, randomGenerateInviteCode, userName, inviterId, telephone);
        Assert.isTrue(save(user), () -> new UserException(UserErrorCode.OPERATION_FAIL));
        // 5.邀请码、手机号存入布隆过滤器
        bloomUtilsBean.getInviteCodeBloomFilter().add(inviteCode);
        bloomUtilsBean.getTelephoneBloomFilter().add(telephone);
        // 6.封装结果返回
        return SingleResponce.of(UserConvertor.INSTANCE.do2VO(getUserByTelephone(telephone)));
    }

    private boolean telephoneExist(String telephone) {
        if (bloomUtilsBean.getTelephoneBloomFilter() != null && bloomUtilsBean.isTelephoneExist(telephone)) {
            return getUserByTelephone(telephone) != null;
        }
        return false;
    }

    public User getUserByTelephone(String telephone) {
        User user = lambdaQuery()
                .eq(User::getTelephone, telephone)
                .one();
        return user;
    }

    private boolean inviteCodeExist(String inviteCode) {
        // 布隆过滤器存在需要再到数据库查询一次
        if (bloomUtilsBean.getInviteCodeBloomFilter() != null && bloomUtilsBean.getInviteCodeBloomFilter().contains(inviteCode)) {
            // 不为空说明存在
            return getUserByInviteCode(inviteCode) != null;
        }
        return false;
    }

    private User getUserByInviteCode(String inviteCode) {
        User user = lambdaQuery()
                .eq(User::getInviteCode, inviteCode)
                .one();
        return user;
    }

    @Transactional
    public void freezeUserByUserId(Long userId) {
        // 1.查询用户，判断用户是否存在，状态是否正常
        User user = getById(userId);
        Assert.notNull(user, () -> new UserException(UserErrorCode.USER_NOT_FOUND));  // 判断通过后，用户被删了，更新数据库会报错
        Assert.isTrue(UserStatus.ACTIVE.name().equals(user.getUserStatus().name()), () -> new UserException(UserErrorCode.USER_STATUS_IS_NOT_ACTIVE)); // todo 判断通过后，用户被改为注销状态
        // todo 此时用户注销了，状态不是ACTIVE了怎么办
        // 2.删除用户缓存
        userCache.remove(userId.toString());
        // 3.更新数据库，并插入用户操作流水表
        user.setUserStatus(UserStatus.FROZEN);
        Assert.isTrue(updateById(user), () -> new UserException(UserErrorCode.OPERATION_FAIL));  // todo hollis定义了更新失败的异常码
        UserOperateStream userOperateStream = new UserOperateStream();
        userOperateStream.record(userId, UserOperateTypeEnum.FREEZE);
        Assert.isTrue(userOperateStreamService.save(userOperateStream), () -> new UserException(UserErrorCode.OPERATION_FAIL));
        // 4.再次删缓存
        userCacheDelayDeleteService.delayedCacheDelete(userCache, user);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        userCache = cacheManager.getOrCreateCache(QuickConfig.newBuilder("user:cache:id")  // 缓存前缀
                .cacheType(CacheType.BOTH)   // 二级缓存：本地缓存+远端缓存
                .expire(Duration.ofHours(1))
                .syncLocal(true)  // 多实例下，当某实例更新/删除key时，通知其他实例缓存失效
                .build()
        );
    }

    // 两个请求进来，第一个删除缓存后，查询新的数据放入缓存，此时第二个请求删除了最新的缓存，但是他最后还是会去查询新的数据放入缓存
    @Transactional
    public void unfreezeUserByUserId(Long userId) {
        // 1.校验用户是否存在，是否为冻结状态
        User user = getById(userId);
        // todo 状态码能否连起来写
        Assert.isTrue(user != null && UserStatus.FROZEN.name().equals(user.getUserRole().name()), () -> new UserException(UserErrorCode.USER_STATUS_IS_NOT_FROZEN_OR_USER_NOT_EXIST));
        // todo 此时用户注销了，状态不是FORZEN了怎么办
        // 2.删除用户缓存
        userCache.remove(userId.toString());
        // 3.修改用户状态为正常，插入流水表
        user.setUserStatus(UserStatus.ACTIVE);
        Assert.isTrue(updateById(user), () -> new UserException(UserErrorCode.OPERATION_FAIL));
        UserOperateStream userOperateStream = new UserOperateStream();
        userOperateStream.record(userId, UserOperateTypeEnum.UNFREEZE);
        Assert.isTrue(userOperateStreamService.save(userOperateStream), () -> new UserException(UserErrorCode.OPERATION_FAIL));
        // 4.再次删除缓存
        userCacheDelayDeleteService.delayedCacheDelete(userCache, user);
    }

    // 此接口由Controller和RPC共享
    public void modifyUserInfo(UserModifyRequest request) {
        // 如果是管理员RPC调用该接口，是不需要知道用户旧密码就可以修改用户信息的；只有用户自己修改自己密码才需要输入自己的旧密码
        // 1.判断用户是否存在
        User user = getById(request.getUserId());
        Assert.notNull(user, () -> new UserException(UserErrorCode.USER_NOT_FOUND));
        // 2.如果有传密码说明用户要修改密码，进行密码校验
        if (StringUtils.isNotEmpty(request.getPassword())){
            Assert.isTrue(StringUtils.equals(DigestUtil.md5Hex(request.getPassword()), user.getPasswordHash()), ()->new UserException(UserErrorCode.PWD_WRONG));
        }
        // 3.修改用户信息
        BeanUtils.copyProperties(request, user);
        Assert.isTrue(updateById(user), () -> new UserException(UserErrorCode.OPERATION_FAIL));
    }
}
